﻿using System;
using System.Globalization;
using System.Reflection;

namespace Fadd
{
	/// <summary>
	/// General purpose class to get/set properties by specifying their name as text.
	/// </summary>
	/// <example>
	/// <code>
	/// User user = new User();
	/// Property.Set(user, "FirstName", "Jonas"); // same as user.FirstName = "Jonas";
	/// Property.Set(user, "CreatedAt", "2008-12-30 14:00"); // the CreatedAt property is of type DateTime, the Property class automatically converts it from string.
	/// Property.Set(user, "Age", "20"); // age is int, which means that it will automatically be converted.
	/// </code>
	/// </example>
	public class Property
	{
		/// <summary>
		/// Assign a property value to the specified object.
		/// </summary>
		/// <param name="item">Object that has the property</param>
		/// <param name="propertyName">Name of the property</param>
		/// <param name="value">Value, this method will try to cast incorrect types (and even try to parse certain types).</param>
		/// <exception cref="ArgumentException">If value is not correct (i.e. null when null is not accepted etc)</exception>
		/// <exception cref="InvalidCastException">If assigner object can't cast to the correct type.</exception>
		/// <exception cref="FormatException">If the value supplied could not be parsed to a proper value for the property</exception>
		public static void Set(object item, string propertyName, object value)
		{
			PropertyInfo property = GetProperty(item, propertyName);
			Type propertyType = property.PropertyType;

			// If the value is null we must first check so that we can assign it
			if(value == null)
			{
				if(propertyType.IsValueType)
					throw new ArgumentException("Cannot assign null value to the property '" + propertyName + "'");

				property.SetValue(item, value, null);
				return;
			}

			Type valueType = value.GetType();

			// Firstly try if we can assign the type directly
			if(property.PropertyType.IsAssignableFrom(valueType))
			{
				property.SetValue(item, value, null);
				return;
			}

			// If the value is a string
			if(value is string)
			{
				string stringValue = (string) value;
				// Parse to date time
				if(propertyType == typeof(DateTime))
				{
					property.SetValue(item, DateTime.Parse(stringValue), null);
					return;
				}

				// Parse to enum
                if(propertyType.IsEnum)
                {
                	property.SetValue(item, Enum.Parse(propertyType, stringValue, true), null);
                	return;
                }

				// Parse to bool
				if(propertyType == typeof(bool))
				{
					if(stringValue == "0" || stringValue.ToLower() == "false")
						property.SetValue(item, false, null);
					else if(stringValue == "1" || stringValue.ToLower() == "true")
						property.SetValue(item, true, null);
					else
						throw new FormatException("Couldn't convert string '" + stringValue + "' to a boolean value for property: " + propertyName);
					
					return;
				}
			}

			// If the value is an integer and we've got an enum property
			if(valueType == typeof(int) && propertyType.IsEnum)
			{
				property.SetValue(item, valueType, null);
				return;
			}

			// Otherwise, if nothing else worked we´ll just have to try and convert it
			property.SetValue(item, Convert.ChangeType(value, propertyType, CultureInfo.CurrentCulture), null);
		}

		/// <summary>
		/// Get a property value from the specified object.
		/// </summary>
		/// <param name="item">Object that has the property</param>
		/// <param name="propertyName">Name of the property</param>
		/// <returns>property value</returns>
		/// <exception cref="ArgumentException">If an invalid property name is given (ie to an indexed property or a property that doesn't exist)</exception>
		public static object Get(object item, string propertyName)
		{
			return GetProperty(item, propertyName).GetValue(item, null);
		}

		/// <summary>
		/// Helper to retrieve a <see cref="PropertyInfo"/> instance
		/// </summary>
		/// <param name="item">The item to retrieve the <see cref="PropertyInfo"/> for</param>
		/// <param name="propertyName">The name of the property to retrieve</param>
		/// <exception cref="ArgumentException">Thrown if the requested property couldn't be found or if the property is indexed</exception>
		/// <returns>The property requested (or throws an <see cref="ArgumentException"/>)</returns>
		private static PropertyInfo GetProperty(object item, string propertyName)
		{
			Check.Require(item, "item");
			Check.NotEmpty(propertyName, "propertyName");

			PropertyInfo propertyInfo = item.GetType().GetProperty(propertyName);
			if (propertyInfo == null)
				throw new ArgumentException("Property '" + propertyName + " could not be found.");

			// If the property is an indexed property the Set method cannot handle it (since no index is provided)
			if (propertyInfo.GetIndexParameters().Length > 0)
				throw new ArgumentException("Property.Set doe not support indexed properties, property name: " + propertyName);

			return propertyInfo;
		}
	}
}
